package panda.server;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

import panda.client.engine.MagicCard;
import panda.client.enumerations.MagicSet;

/**
 * Reads decklists from file and constructs them into DeckEntry lists.
 * 
 * @author zane.tian@gmail.com
 */
public class DeckScanner {
	private List<MagicCard> cardDB;

	public DeckScanner(List<MagicCard> serverCardDB) {
		this.cardDB = serverCardDB;
	}

	/**
	 * Reads the contents of a deck.txt file in Apprentice format. Each line in the file is in the format:<br>
	 * "n cardName"<br>
	 * Examples include "15 Forest" and "3 Deus of Calamity". This method skips the sideboard cards, blank lines, and
	 * comments beginning with "//"
	 */
	private List<DeckScannerLine> getDeckScannerLinesFromFile(String filename) {
		String root;
		List<DeckScannerLine> deckdata = new ArrayList<DeckScannerLine>();

		if(new File(DataServiceImpl.kyruruDotNetRoot).exists()) {
			root = DataServiceImpl.kyruruDotNetRoot;
		} else if(new File(DataServiceImpl.hostedModeRoot).exists()) {
			root = DataServiceImpl.hostedModeRoot;
		} else {
			throw new IllegalStateException("Neither kyruru.net nor hosted mode were found.");
		}

		try {
			Scanner scanner = new Scanner(new File(root + "/" + filename), "ISO-8859-1"); /* TODO: why not UTF8? */
			String cardLine;
			String cardName;
			int firstSpacePosition;
			int numCards;

			/* convert each text file line into a deckentry. skip the sideboard */
			while(scanner.hasNext()) {
				cardLine = scanner.nextLine();
				/* skip blank lines and comment lines */
				if(cardLine.equals("") || cardLine.substring(0, 2).equals("//")) {
					continue;
				}

				if(cardLine.toLowerCase().contains("sideboard")) {
					break;
				}

				/* apprentice format is "n cardname". we split n and cardname here */
				firstSpacePosition = cardLine.indexOf(' ');
				numCards = Integer.parseInt(cardLine.substring(0, firstSpacePosition));
				cardName = cardLine.substring(firstSpacePosition + 1);

				deckdata.add(new DeckScannerLine(numCards, cardName));
			}
		} catch(Exception ex) {
			ex.printStackTrace();
		}

		return deckdata;
	}

	/**
	 * Grabs deck information from a file and returns the assembled decklist.
	 */
	public List<DeckEntry> assembleDecklistFromFile(String filename) {
		/* create card list from file */
		List<DeckEntry> decklist = new ArrayList<DeckEntry>();

		final List<String> landsList = new ArrayList<String>();
		landsList.add("forest");
		landsList.add("island");
		landsList.add("swamp");
		landsList.add("plains");
		landsList.add("mountain");

		List<DeckScannerLine> deckdata = getDeckScannerLinesFromFile(filename);
		int cardsInDeckData = 0;

		/* find out how many cards are in deckdata. add basic lands from deckdata to lands. */
		List<DeckScannerLine> lands = new ArrayList<DeckScannerLine>();
		for(DeckScannerLine line : deckdata) {
			cardsInDeckData += line.getNumCards();
			if(landsList.contains(line.getCardName().toLowerCase())) {
				lands.add(line);
				// System.out.println("added " + line);
			}
		}

		/* remove all the found basic lands from deckdata */
		for(DeckScannerLine line : lands) {
			deckdata.remove(line);
		}

		/**
		 * Special rules for cards with the same name but different artwork:<br>
		 * Basic Lands - Forest Mountain Plains Island Swamp<br>
		 * Only basic lands of the same type: random between all artwork<br>
		 * Basic lands of more than 1 type: match artwork from most numerous 2 lands, random for others<br>
		 * Set basic lands to only be valid if within a specific 'big' set: lorwyn or shadowmoor <br>
		 * TODO what about non-'basic land' cards with the same name or cards printed across sets?<br>
		 * TODO maybe add a date for card sets and pick the most recent set or limit sets in text file.
		 */
		MagicSet matchSet = MagicSet.SHARDS_OF_ALARA;
		
		/** Adds lands with matching artwork from Shadowmoor */
		// switch(lands.size()) {
		// case 1:
		// addRandomLandArtworkToDecklist(cardDB, lands.get(0), matchSet, decklist);
		// break;
		// case 2: /* match artists for lands for the 2 most numerous lands, then randomly pick the others */
		// case 3:
		// case 4:
		// case 5:
		// Collections.sort(lands);
		// addMatchingLandArtworkToDecklist(lands.get(0), lands.get(1), matchSet, cardDB, decklist);
		//
		// /* add the rest of the lands at random */
		// lands.remove(0);
		// lands.remove(0);
		// for(DeckScannerLine line : lands) {
		// addRandomLandArtworkToDecklist(cardDB, line, matchSet, decklist);
		// }
		// break;
		//
		// default:
		// // throw new IllegalStateException("There should be only 5 basic land types: " + lands.size());
		// }
		
		/** Randomly add lands */
		for(DeckScannerLine line : lands) {
			addRandomLandArtworkToDecklist(cardDB, line, matchSet, decklist);
		}
		

		/* add all the non-basic lands to the decklist */
		for(DeckScannerLine line : deckdata) {
			MagicCard target = null;
			String cardName = /* line.getCardName().contains("thertow") ? "Aethertow" : */line.getCardName().trim();
			int numCards = line.getNumCards();

			/* search within loaded cards and break on first match */
			assert !landsList.contains(cardName) : "There should be no lands in deckdata at this point";

			for(MagicCard card : cardDB) {
				if(cardName.toLowerCase().equals(card.getName().toLowerCase())) {
					target = card;
					break;
				}
			}

			assert target != null : "there should have been a match for " + cardName + ".";

			if(target == null) {
				System.out.println(cardName + " not found in the magic card database");
			}

			/* add the deckentry to the list to be processed later */
			decklist.add(new DeckEntry(numCards, target.getName(), target.getID(), target.getMagicSet().getAbbrev()));

		}

		int cardsInDeckList = 0;
		for(DeckEntry entry : decklist) {
			cardsInDeckList += entry.getCopies();
		}

		assert cardsInDeckData == cardsInDeckList : "The number of cards originally read from file (" + cardsInDeckData
				+ ") differ from the number of cards added to decklist (" + cardsInDeckList + ")";

		return decklist;
	}

	/**
	 * Adds the two lands to a decklist. The lands chosen (there are many same-name lands with different artwork) are the
	 * ones where both lands share the same artist.
	 * 
	 * @param land1: what the most frequent basic land is in the deck
	 * @param land2: what the second most frequent basic land is in the deck
	 * @param magicCardDB: the list of all the cards in the DB.
	 * @param matchSet: the magic set we want to add lands from - for example, Shadowmoor or Lorwyn.
	 * @param decklist: the deck (List<DeckEntry> we want to add the lands to.
	 */
	@SuppressWarnings("all")
	private void addMatchingLandArtworkToDecklist(DeckScannerLine land1, DeckScannerLine land2, MagicSet matchSet,
			List<MagicCard> magicCardDB, List<DeckEntry> decklist) {

		/* put all copies of the most numerous land from the card DB into a list */
		List<MagicCard> land1Copies = new ArrayList<MagicCard>();
		for(MagicCard card : magicCardDB) {
			boolean setMatches = card.getMagicSet().equals(matchSet);
			boolean nameMatches = land1.getCardName().toLowerCase().equals(card.getName().toLowerCase());
			if(setMatches && nameMatches) {
				land1Copies.add(card);
			}
		}

		/* put all copies of the second most numerous land from the card DB into a list */
		List<MagicCard> land2Copies = new ArrayList<MagicCard>();
		for(MagicCard card : magicCardDB) {
			boolean setMatches = card.getMagicSet().equals(matchSet);
			boolean nameMatches = land2.getCardName().toLowerCase().equals(card.getName().toLowerCase());
			if(setMatches && nameMatches) {
				land2Copies.add(card);
			}
		}

		/* match artists */
		boolean found = false;
		MagicCard land1ArtMatch = null;
		MagicCard land2ArtMatch = null;
		for(MagicCard card : land1Copies) {
			for(MagicCard secondCard : land2Copies) {
				if(card.getIllustrator().equals(secondCard.getIllustrator())) {
					found = true;
					land1ArtMatch = card;
					land2ArtMatch = secondCard;
					break;
				}
			}
		}

		assert found : "the 2 most numerous lands do not share a common illustrator o.O  Wrong set maybe?";

		/* add 2 most numerous land (now matching!) to decklist */
		decklist.add(new DeckEntry(land1.getNumCards(), land1ArtMatch.getName(), land1ArtMatch.getID(), land1ArtMatch
				.getMagicSet().getAbbrev()));
		decklist.add(new DeckEntry(land2.getNumCards(), land2ArtMatch.getName(), land2ArtMatch.getID(), land2ArtMatch
				.getMagicSet().getAbbrev()));
	}

	/**
	 * There are multiple lands of the same card name with unique artwork. This method adds randomly selected lands of
	 * the name to a deck.
	 * 
	 * @param magicCardDB: the list of all the cards in the DB.
	 * @param line: a DeckScannerLine holding basic land information.
	 * @param matchSet: the magic set we want to add lands from - for example, Shadowmoor or Lorwyn.
	 * @param deckEntries: the deck (List<DeckEntry> we want to add the lands to.
	 */
	private void addRandomLandArtworkToDecklist(List<MagicCard> magicCardDB, DeckScannerLine line, MagicSet matchSet,
			List<DeckEntry> deckEntries) {
		String cardName = line.getCardName();
		int numCards = line.getNumCards();

		/* pick out all the cards with the same name as 'line'.getCardName and stuff them into a list */
		List<MagicCard> landsFound = new ArrayList<MagicCard>();
		for(MagicCard card : magicCardDB) {
			boolean setMatches = card.getMagicSet().equals(matchSet);
			boolean nameMatches = cardName.toLowerCase().equals(card.getName().toLowerCase());
			if(setMatches && nameMatches) {
				landsFound.add(card);
			}
		}

		/* pick elements from the list at random to add to the deck */
		MagicCard target = null;
		for(int i = 0; i < numCards; i++) {
			target = landsFound.get(getRandInt(landsFound.size() - 1));
			deckEntries.add(new DeckEntry(1, target.getName(), target.getID(), target.getMagicSet().getAbbrev()));
		}
	}

	/** Returns a random integer in the range [0, max] */
	private int getRandInt(int max) {
		return (int) Math.round(Math.random() * max);
	}
}